package il.ac.bgu.cs.heuristics.graph.pddl; // NOPMD high degree of imports

import il.ac.bgu.cs.heuristics.graph.State;
import il.ac.bgu.cs.heuristics.graph.Substitution;
import il.ac.bgu.cs.heuristics.graph.pddl.auxiliary.ContradictionResolver;
import il.ac.bgu.cs.heuristics.graph.pddl.parser.ExpressionParser;
import il.ac.bgu.cs.heuristics.graph.pddl.parser.types.AbstractParameter;
import il.ac.bgu.cs.heuristics.graph.pddl.parser.types.Action;
import il.ac.bgu.cs.heuristics.graph.pddl.parser.types.BoundParameter;
import il.ac.bgu.cs.heuristics.graph.pddl.parser.types.ITypedConstant;
import il.ac.bgu.cs.heuristics.graph.pddl.parser.types.LogicalExpression;
import il.ac.bgu.cs.heuristics.graph.pddl.parser.types.Predicate;
import il.ac.bgu.cs.heuristics.graph.pddl.parser.types.Type;
import il.ac.bgu.cs.heuristics.graph.types.IState;
import il.ac.bgu.cs.heuristics.graph.types.ISubstitution;
import il.ac.bgu.cs.heuristics.utils.CollectionUtils;
import il.ac.bgu.cs.heuristics.utils.Pair;
import il.ac.bgu.cs.heuristics.utils.StringUtils;
import il.ac.bgu.cs.heuristics.utils.Validation;

import java.io.File;
import java.io.FileNotFoundException;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.log4j.Logger;

import pddl4j.Domain;
import pddl4j.ErrorManager;
import pddl4j.PDDLObject;
import pddl4j.Parser;
import pddl4j.Problem;
import pddl4j.RequireKey;
import pddl4j.exp.AtomicFormula;
import pddl4j.exp.action.ActionDef;
import pddl4j.exp.term.Constant;
import pddl4j.exp.term.Term;

/**
 * 
 * @author Ronen Pinko
 * 
 */
public final class PddlUtils { // NOPMD too many methods
	private final static Logger					LOG								= Logger
																						.getLogger(PddlUtils.class);

	/**
	 * Pattern of types data in a pddl domain
	 */
	private static final Pattern				TYPE_DEFINITION_PATTERN			= Pattern
																						.compile("\\(:types (.*?)\\)\\s*\\(:");
	// .compile("\\(:types (.*?)\\)");

	private static final Pattern				REGULAR_TYPE_DEFINITION_PATTERN	= Pattern
																						.compile("(\\w+) - (\\w+)");

	private static final Pattern				TREE_TYPE_DEFINITION_PATTERN	= Pattern
																						.compile("(\\w+) - \\(either (\\w+) (\\w+)\\)");

	/**
	 * Group number of types data found by {@link #TYPE_DEFINITION_PATTERN}
	 */
	private static final int					TYPE_DEFINITION_PATTERN_GROUP	= 1;

	private static final Set<PddlRequirement>	DOMAIN_REQUIREMENTS				= CollectionUtils
																						.unmodifiableSet(
																								PddlRequirement.STRIPS,
																								PddlRequirement.TYPING);
	private static final Set<PddlRequirement>	PROBLEM_REQUIREMENTS			= CollectionUtils
																						.unmodifiableSet(PddlRequirement.STRIPS);

	private static enum PddlRequirement {
		STRIPS(":strips"), TYPING(":typing"), UNDEFINED("undefined");
		private final String	stringRep;

		private PddlRequirement(String stringRep) {
			this.stringRep = stringRep;
		}

		@Override
		public String toString() {
			return stringRep;
		}

		static PddlRequirement from(String s) {
			Validation.ensureArgumentContainsText(s);
			for (PddlRequirement req : values()) {
				if (req.stringRep.equals(s)) {
					return req;
				}
			}
			return UNDEFINED;
		}
	}

	private PddlUtils() {
	} // no instantiation

	public static boolean areTypesSame(List<ITypedConstant> constants,
			List<AbstractParameter> parameters) {
		Validation.ensureAreSameSize(constants, parameters);
		for (int i = 0; i < constants.size(); i++) {
			final ITypedConstant constant = constants.get(i);
			final AbstractParameter parameter = parameters.get(i);
			if (!parameter.hasSameType(constant)) {
				return false;
			}
		}
		return true;
	}

	public static ISubstitution createSubstitution(Predicate predicate,
			ContradictionResolver resolver,
			List<AbstractParameter> actionParameters,
			List<ITypedConstant> constants) {
		final Map<String, ITypedConstant> paramLookup = createActionParamNameToConstantMap(
				actionParameters, constants);
		final Substitution.Builder builder = new Substitution.Builder(
				predicate, resolver);
		for (AbstractParameter parameter : predicate.getParameters()) {
			if (parameter.isFree()) {
				builder.addConstant(paramLookup.get(parameter.name()));
			} else {
				builder.addConstant(SafeCast.toTypedConstant(parameter));
			}
		}
		return builder.build();
	}

	/**
	 * Creates a lookup table by parameter name
	 * 
	 * @param actionParameters
	 */
	private static Map<String, ITypedConstant> createActionParamNameToConstantMap(
			List<AbstractParameter> actionParameters,
			List<ITypedConstant> constants) {
		Validation.ensureAreSameSize(actionParameters, constants);
		final Map<String, ITypedConstant> lookupTable = CollectionUtils
				.newHashMap();
		int i = 0;
		for (ITypedConstant constant : constants) {
			final BoundParameter param = SafeCast.toBoundParameter(constant);
			lookupTable.put(actionParameters.get(i++).name(), param);
		}
		return lookupTable;
	}

	public static IState createState(ContradictionResolver resolver,
			LogicalExpression expression,
			List<AbstractParameter> actionParameters,
			List<ITypedConstant> constants) {
		final State.Builder preconStateBuilder = new State.Builder();

		for (Predicate predicate : expression.getPredicates()) {
			preconStateBuilder.addSubstitution(PddlUtils.createSubstitution(
					predicate, resolver, actionParameters, constants));
		}
		return preconStateBuilder.build();
	}

	public static Collection<ITypedConstant> extractTypedConstants(
			Domain domain, Problem problem, Set<Type> typeSet) {
		final List<ITypedConstant> typedConstants = createTypedConstantList(CollectionUtils
				.toIterable(domain.constantsIterator()));
		typedConstants.addAll(createTypedConstantList(CollectionUtils
				.toIterable(problem.constantsIterator())));
		return Collections.unmodifiableList(typedConstants);

	}

	private static List<ITypedConstant> createTypedConstantList(
			Iterable<Constant> constants) {
		final List<ITypedConstant> typedConstants = CollectionUtils
				.newArrayList();
		for (Constant c : constants) {
			final AbstractParameter parameter = ExpressionParser
					.parseTypedParameter(c.toTypedString());
			if (parameter instanceof ITypedConstant) {
				typedConstants.add((BoundParameter) parameter);
			} else {
				throw new IllegalArgumentException(
						"typed constant should be bound - " + c.toTypedString());
			}
		}

		return typedConstants;
	}

	/**
	 * Extracts type definitions from a PDDL domain object
	 * 
	 * @param domain
	 * @return
	 * @throws IllegalArgumentException
	 *             if discovers use of sub typing or couldn't parse the types
	 */
	public static Set<Type> extractTypeSet1(Domain domain) {
		// TODO: unfortunately PDDL4J doesn't expose a method to do this
		// extraction, should find a better way to extract

		final String domainStr = domain.toString().replaceAll("\n", "");
		final Matcher typeMatcher = TYPE_DEFINITION_PATTERN.matcher(domainStr);
		if (typeMatcher.find()) {
			String typeDataStr = typeMatcher.group(
					TYPE_DEFINITION_PATTERN_GROUP).replaceFirst("\\s*object",
					"").trim();
			while (typeDataStr.contains("  ")) {
				typeDataStr = typeDataStr.replaceAll("  ", " ");
			}
			final String[] types = typeDataStr.split(" - \\w+ ?");
			final String[] parents = typeDataStr.replaceFirst("\\w+ - ", "")
					.split(" \\w+ - ");
			for (String parent : parents) {
				if (parent == null) {
					throw new IllegalArgumentException(
							"pattern matching failed, got null type");
				}
				final String parentType = parent.trim();
				if (StringUtils.containsText(parentType)
						&& !Type.OBJECT.name().equals(parentType)) {
					throw new IllegalArgumentException(
							"doesn't support subtypes. found sub type - '"
									+ parent + "'");
				}
			}
			final Set<Type> typeSet = CollectionUtils.newLinkedHashSet();
			for (String type : types) {
				if (type == null) {
					throw new IllegalArgumentException(
							"pattern matching failed, got null type");
				}
				typeSet.add(Type.from(type.trim()));
			}
			return Collections.unmodifiableSet(typeSet);

		} else {
			throw new IllegalArgumentException(
					"can't parse domain type definitions");
		}
	}

	public static void validatePddl(Domain domain, Problem problem) {
		LOG.debug("validating domain");
		ensurePddlMeetsRequirements(CollectionUtils.toIterable(domain
				.requirementsIterator()), DOMAIN_REQUIREMENTS);
		LOG.debug("validating problem");
		ensurePddlMeetsRequirements(CollectionUtils.toIterable(problem
				.requirementsIterator()), PROBLEM_REQUIREMENTS);
	}

	public static void ensurePddlMeetsRequirements(Iterable<RequireKey> reqIt,
			Set<PddlRequirement> requirements) {
		final Map<PddlRequirement, Boolean> reqCheckList = CollectionUtils
				.newHashMap();
		for (PddlRequirement req : requirements) {
			reqCheckList.put(req, Boolean.FALSE);
		}
		for (RequireKey key : reqIt) {
			reqCheckList
					.put(PddlRequirement.from(key.toString()), Boolean.TRUE);
		}
		for (Entry<PddlRequirement, Boolean> entry : reqCheckList.entrySet()) {
			if (entry.getValue().equals(Boolean.FALSE)) {
				throw new IllegalArgumentException("missing PDDL requirement '"
						+ entry.getKey() + "'");
			}
		}
	}

	/**
	 * @return never {@code null}
	 */
	public static List<AbstractParameter> extractActionParameters(
			pddl4j.exp.action.Action action) {
		final List<AbstractParameter> parameters = CollectionUtils
				.newArrayList();
		for (Term term : action.getParameters()) {
			parameters.add(ExpressionParser.parseTypedParameter(term
					.toTypedString()));
		}
		return Collections.unmodifiableList(parameters);
	}

	public static List<Action> extractActions(Domain domain) {
		final List<Action> actions = CollectionUtils.newArrayList();
		final Iterator<ActionDef> actionIterator = domain.actionsIterator();
		while (actionIterator.hasNext()) {
			final pddl4j.exp.action.Action pddlAction = (pddl4j.exp.action.Action) actionIterator
					.next();
			actions.add(new Action(pddlAction)); // NOPMD instantiate in loop
		}
		return Collections.unmodifiableList(actions);
	}

	/**
	 * @return never {@code null}
	 */
	public static List<Predicate> extractPredicates(Domain domain,
			Map<Type, List<Type>> typesHierarchy) {
		final List<Predicate> predicates = CollectionUtils.newArrayList();
		final Iterator<AtomicFormula> predicatesIterator = domain
				.predicatesIterator();
		Predicate predicate = null;
		while (predicatesIterator.hasNext()) {
			predicate = ExpressionParser.parsePredicate(predicatesIterator
					.next().toTypedString());
			predicate.createTypeHierarchyForParams(typesHierarchy);
			predicates.add(predicate);
		}
		return Collections.unmodifiableList(predicates);
	}

	public static PDDLObject compilePDDL(File f) throws FileNotFoundException {
		Validation.ensureArgumentNeverNull(f);
		PDDLObject pddlObj = null;
		final ErrorManager errManager = new ErrorManager();
		final Parser parser = new Parser(errManager);
		pddlObj = parser.parse(f);
		if (pddlObj == null) {
			for (String errMessage : errManager.getMessages(f)) {
				LOG.error("got error while parsing pddl - " + errMessage);
			}
		}
		return pddlObj;
	}

	/**
	 * Extracts type definitions from a PDDL domain object
	 * 
	 * @param domain
	 * @return
	 * @throws IllegalArgumentException
	 *             if discovers use of sub typing or couldn't parse the types
	 */
	public static Set<Type> extractTypeSet(Domain domain) {
		// TODO: unfortunately PDDL4J doesn't expose a method to do this
		// extraction, should find a better way to extract

		final String domainStr = domain.toString().replaceAll("\n", "");
		final Matcher typeMatcher = TYPE_DEFINITION_PATTERN.matcher(domainStr);
		if (typeMatcher.find()) {
			String typeDataStr = typeMatcher.group(
					TYPE_DEFINITION_PATTERN_GROUP).replaceFirst("\\s*object",
					"").trim();
			while (typeDataStr.contains("  ")) {
				typeDataStr = typeDataStr.replaceAll("  ", " ");
			}

			Collection<Pair<String>> typeParentPairs = CollectionUtils
					.newArrayList();
			final Matcher regularTypeMatcher = REGULAR_TYPE_DEFINITION_PATTERN
					.matcher(typeDataStr);
			while (regularTypeMatcher.find()) {
				typeParentPairs.add(new Pair<String>(regularTypeMatcher
						.group(1), regularTypeMatcher.group(2)));
			}

			final Matcher treeTypeMatcher = TREE_TYPE_DEFINITION_PATTERN
					.matcher(typeDataStr);
			while (treeTypeMatcher.find()) {
				typeParentPairs.add(new Pair<String>(treeTypeMatcher.group(1),
						treeTypeMatcher.group(3)));
			}

			final Set<Type> typeSet = CollectionUtils.newLinkedHashSet();
			for (Pair<String> type : typeParentPairs) {
				if (type == null) {
					throw new IllegalArgumentException(
							"pattern matching failed, got null type");
				}
				if (!StringUtils.containsText(type.getFirst())
						|| !StringUtils.containsText(type.getSecond())) {
					throw new IllegalArgumentException(
							"pattern matching failed, empty strings");
				}
				typeSet.add(new Type(type.getFirst(), Type.from(type
						.getSecond())));
			}
			return Collections.unmodifiableSet(typeSet);
		} else {
			throw new IllegalArgumentException(
					"can't parse domain type definitions");
		}
	}

}
